// Package redis_lock
// Created by GoLand
// @User: lenora
// @Date: 2024/8/13
// @Time: 16:59

package redis_lock

import (
	"context"
	_ "embed"
	"errors"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"time"
)

var (
	//go:embed script/lua/unlock.lua
	luaUnlock string
	//go:embed script/lua/refresh.lua
	luaRefresh string
	//go:embed script/lua/lock.lua
	luaLock string
)

var (
	ErrLockNotHold = errors.New("not hold the lock")
	ErrLockFailed  = errors.New("add lock failed")
)

type Client struct {
	client redis.Cmdable
}

func NewClient(client redis.Cmdable) *Client {
	return &Client{client: client}
}

// Lock  加锁
//
// @param ctx 上下文信息
// @param key 锁key值
// @param expiration key有效期
// @param strategy 重试机制
// @param timeout 单次重试的超时限制
// @return *Lock 锁信息
// @return error:
//
//   - context.DeadlineExceeded: Lock整体调用超时,加锁是否成功未知
//   - ErrLockFailed: 加锁失败,且超过重试次数
func (c *Client) Lock(ctx context.Context, key string, expiration time.Duration, strategy RetryStrategy, timeout time.Duration) (*Lock, error) {
	value := uuid.New().String()
	var timer *time.Timer
	defer func() {
		if timer != nil {
			timer.Stop()
		}
	}()
	for {
		luaCtx, cancel := context.WithTimeout(context.Background(), timeout)
		res, err := c.client.Eval(luaCtx, luaLock, []string{key}, value, expiration).Result()
		cancel()
		if err != nil && !errors.Is(err, context.DeadlineExceeded) {
			//非请求超时的情况返回错误
			return nil, err
		}
		if res == "OK" {
			//执行成功跳出循环,正常返回
			return newLock(c.client, key, value, expiration), nil
		}
		//执行失败,进入重试机制
		interval, ok := strategy.Next()
		if !ok {
			//超出重试次数,加锁失败
			return nil, ErrLockFailed
		}
		//开启定时执行器
		if timer == nil {
			timer = time.NewTimer(interval)
		} else {
			timer.Reset(interval)
		}

		select {
		case <-timer.C:
			//定时器触发,进入下一次循环,开始重试
		case <-ctx.Done():
			//加锁请求超时,返回错误
			return nil, ctx.Err()
		}
	}
}

func (c *Client) TryLock(ctx context.Context, key string, expiration time.Duration) (*Lock, error) {
	value := uuid.New().String()
	res, err := c.client.SetNX(ctx, key, value, expiration).Result()
	if err != nil {
		return nil, err
	}
	if !res {
		return nil, ErrLockFailed
	}
	return newLock(c.client, key, value, expiration), nil
}

func newLock(client redis.Cmdable, key, value string, expiration time.Duration) *Lock {
	return &Lock{
		client:     client,
		key:        key,
		value:      value,
		expiration: expiration,
		unlock:     make(chan struct{}, 1),
	}
}

type Lock struct {
	client     redis.Cmdable
	key        string
	value      string
	expiration time.Duration
	unlock     chan struct{}
}

// Unlock 解锁
// 解锁时要确保是自己加的锁
//  1. 引入验证value的机制
//  2. 保证获取value后再删除的操作是原子性的,使用lua脚本
func (l *Lock) Unlock(ctx context.Context) error {
	defer func() {
		//解锁,结束续约
		l.unlock <- struct{}{}
		//关闭channel,避免多次unlock
		close(l.unlock)
	}()
	res, err := l.client.Eval(ctx, luaUnlock, []string{l.key}, l.value).Int64()
	if errors.Is(err, redis.Nil) {
		//get key时发生错误
		return ErrLockNotHold
	}
	if err != nil {
		return err
	}
	if res != 1 {
		//1. key过期
		//2. key不存在
		//3. key不是当前lock的
		return ErrLockNotHold
	}
	return nil
}

func (l *Lock) Refresh(ctx context.Context) error {
	res, err := l.client.Eval(ctx, luaRefresh, []string{l.key}, l.value, l.expiration.Milliseconds()).Int64()
	if errors.Is(err, redis.Nil) {
		//get key时发生错误
		return ErrLockNotHold
	}
	if err != nil {
		return err
	}
	if res != 1 {
		//1. key过期
		//2. key不存在
		//3. key不是当前lock的
		return ErrLockNotHold
	}
	return nil
}

// AutoRefresh  自动续约
//
// @param interval	续约间隔,多久续约一次
// @param timeout	超时时间,续约执行的超时
// @return error
func (l *Lock) AutoRefresh(interval, timeout time.Duration) error {
	ch := make(chan struct{}, 1)
	ticker := time.NewTicker(interval)
	defer func() {
		ticker.Stop()
		close(ch)
	}()
	for {
		select {
		//续约失败,重试
		case <-ch:
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := l.Refresh(ctx)
			cancel()
			if errors.Is(err, context.DeadlineExceeded) {
				ch <- struct{}{}
				continue
			}
			if err != nil {
				return err
			}
		//正常续约
		case <-ticker.C:
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			err := l.Refresh(ctx)
			cancel()
			//refresh执行超时
			if errors.Is(err, context.DeadlineExceeded) {
				//立即重试
				ch <- struct{}{}
				continue
			}
			if err != nil {
				return err
			}
		//已经unlock,结束循环
		case <-l.unlock:
			return nil
		}
	}

}
